#ifndef OPENCLMANAGER_H
#define OPENCLMANAGER_H

#include "CL/cl.hpp"
//#include "CLKERNEL/clkernels.h"
#include <stdio.h>
#include <QObject>

class OpenCLManager: public QObject
{
    Q_OBJECT

public:
    cl_int                          lastErrorCode;

    std::vector<cl::Platform>       platforms;
    std::vector<cl::Context>        contexts;
    std::vector<cl::Device>         *devices        = nullptr;
    std::vector<cl::CommandQueue>   *commandQueues  = nullptr;

    std::vector<cl::Program>        programs;
    std::vector<cl::Kernel>         kernels;

private:
    OpenCLManager()
    {
        // get all avaliable platforms (Intel, AMD, NVidia, etc.)
        lastErrorCode = cl::Platform::get(&platforms);
        if (lastErrorCode != CL_SUCCESS) return;

        // get all avaliable devices per platform
        /// \todo put it after creating the context - get devices from context!
        devices = new std::vector<cl::Device> [platforms.size()];
        for(unsigned i=0; i<platforms.size();++i)
        {
            lastErrorCode = platforms[i].getDevices(CL_DEVICE_TYPE_ALL, &devices[i]);
            if (lastErrorCode != CL_SUCCESS) return;
        }

        // create context for devices per platform
        for(unsigned i=0; i<platforms.size();++i)
        {
            cl_context_properties properties[] = {
                CL_CONTEXT_PLATFORM,
                (cl_context_properties)(platforms[i])(),
                0};
            contexts.push_back(cl::Context(devices[i],properties));
        }

        // create command queues for each device
        commandQueues = new std::vector<cl::CommandQueue> [platforms.size()];
        for(unsigned i=0; i<platforms.size();++i)
            for(unsigned j=0; j<devices[i].size();++j)
                commandQueues[i].push_back(cl::CommandQueue(contexts[i],devices[i][j]));
    }

    public: QString getPlatformsInfo() const
    {
        if(!platforms.empty())
        {
            QString _str = "Avaliable OpenCL platforms:\n";
            for(unsigned i=0; i<platforms.size();++i)
            {
                _str += "platform["+ QString::number(i) + "]:\n";
                cl::STRING_CLASS data;
                platforms[i].getInfo(CL_PLATFORM_PROFILE,&data);
                _str += "\tCL_PLATFORM_PROFILE:    " + QString(data.c_str()) + "\n";
                platforms[i].getInfo(CL_PLATFORM_VERSION,&data);
                _str += "\tCL_PLATFORM_VERSION:    " + QString(data.c_str()) + "\n";
                platforms[i].getInfo(CL_PLATFORM_NAME,&data);
                _str += "\tCL_PLATFORM_NAME:       " + QString(data.c_str()) + "\n";
                platforms[i].getInfo(CL_PLATFORM_VENDOR,&data);
                _str += "\tCL_PLATFORM_VENDOR:     " + QString(data.c_str()) + "\n";
                platforms[i].getInfo(CL_PLATFORM_EXTENSIONS,&data);
                _str += "\tCL_PLATFORM_EXTENSIONS: " + QString(data.c_str()) + "\n";
            }
            return _str + "\n";
        }
        else return "No avaliable OpenCL platforms\n";
    }

    private: template<typename T> static void _appendBitfield(
      const T info, const T value, const cl::STRING_CLASS name, cl::STRING_CLASS &str)
    {
       if (info & value)
       {
          if (str.length() > 0)
          {
             str.append(" | ");
          }
          str.append(name);
       }
    }
    public: QString getDevicesInfo()
    {
        if(!platforms.empty() && devices!= nullptr)
        {
            QString _str;
            for(unsigned i=0; i<platforms.size();++i)
            {
                _str += "platform["+ QString::number(i) + "]:";
                for(unsigned j=0; j<devices[i].size();++j)
                {
                    _str += "device["+ QString::number(j) + "]:\n";
                    cl::STRING_CLASS data;

                    cl_device_type type;
                    devices[i][j].getInfo(CL_DEVICE_TYPE, &type);
                    _appendBitfield<cl_device_type>(type, CL_DEVICE_TYPE_CPU, "CL_DEVICE_TYPE_CPU", data);
                    _appendBitfield<cl_device_type>(type, CL_DEVICE_TYPE_GPU, "CL_DEVICE_TYPE_GPU", data);
                    _appendBitfield<cl_device_type>(type, CL_DEVICE_TYPE_ACCELERATOR, "CL_DEVICE_TYPE_ACCELERATOR", data);
                    _appendBitfield<cl_device_type>(type, CL_DEVICE_TYPE_DEFAULT, "CL_DEVICE_TYPE_DEFAULT", data);
                    _str += "\tCL_DEVICE_TYPE:                     " + QString(data.c_str()) + "\n";

                    cl_uint number;
                    devices[i][j].getInfo(CL_DEVICE_VENDOR_ID, &number);
                    _str += "\tCL_DEVICE_VENDOR_ID:                " + QString::number(number) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_MAX_COMPUTE_UNITS, &number);
                    _str += "\tCL_DEVICE_MAX_COMPUTE_UNITS:        " + QString::number(number) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS, &number);
                    _str += "\tCL_DEVICE_MAX_WORK_ITEM_DIMENSIONS: " + QString::number(number) + "\n";

                    /// \todo
                    //devices[i][j].getInfo(CL_DEVICE_MAX_WORK_ITEM_SIZES, &data);
                    //_str += "\tCL_DEVICE_MAX_WORK_ITEM_SIZES:      " + QString(data.c_str()) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_MAX_WORK_GROUP_SIZE, &number);
                    _str += "\tCL_DEVICE_MAX_WORK_GROUP_SIZE:      " + QString::number(number) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_MAX_CLOCK_FREQUENCY, &number);
                    _str += "\tCL_DEVICE_MAX_CLOCK_FREQUENCY:      " + QString::number(number) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_ADDRESS_BITS, &number);
                    _str += "\tCL_DEVICE_ADDRESS_BITS:             " + QString::number(number) + "\n";

                    cl_ulong longNumber;
                    devices[i][j].getInfo(CL_DEVICE_MAX_MEM_ALLOC_SIZE, &longNumber);
                    _str += "\tCL_DEVICE_MAX_MEM_ALLOC_SIZE:       " + QString::number(longNumber) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_MAX_PARAMETER_SIZE, &number);
                    _str += "\tCL_DEVICE_MAX_PARAMETER_SIZE:       " + QString::number(number) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_MEM_BASE_ADDR_ALIGN, &number);
                    _str += "\tCL_DEVICE_MEM_BASE_ADDR_ALIGN:      " + QString::number(number) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_MIN_DATA_TYPE_ALIGN_SIZE, &number);
                    _str += "\tCL_DEVICE_MIN_DATA_TYPE_ALIGN_SIZE: " + QString::number(number) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_GLOBAL_MEM_SIZE, &longNumber);
                    _str += "\tCL_DEVICE_GLOBAL_MEM_SIZE:          " + QString::number(longNumber) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_LOCAL_MEM_SIZE, &longNumber);
                    _str += "\tCL_DEVICE_LOCAL_MEM_SIZE:           " + QString::number(longNumber) + "\n";

                    cl_bool b;
                    devices[i][j].getInfo(CL_DEVICE_HOST_UNIFIED_MEMORY, &b);
                    data = b ? "TRUE" : "FALSE";
                    _str += "\tCL_DEVICE_HOST_UNIFIED_MEMORY:      " + QString(data.c_str()) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_AVAILABLE, &b);
                    data = b ? "TRUE" : "FALSE";
                    _str += "\tCL_DEVICE_AVAILABLE:                " + QString(data.c_str()) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_COMPILER_AVAILABLE, &b);
                    data = b ? "TRUE" : "FALSE";
                    _str += "\tCL_DEVICE_COMPILER_AVAILABLE:       " + QString(data.c_str()) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_NAME, &data);
                    _str += "\tCL_DEVICE_NAME:              " + QString(data.c_str()) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_VENDOR, &data);
                    _str += "\tCL_DEVICE_VENDOR:                   " + QString(data.c_str()) + "\n";

                    devices[i][j].getInfo(CL_DRIVER_VERSION, &data);
                    _str += "\tCL_DRIVER_VERSION:                  " + QString(data.c_str()) + "\n";

                    devices[i][j].getInfo(CL_DEVICE_EXTENSIONS, &data);
                    _str += "\tCL_DEVICE_EXTENSIONS:               " + QString(data.c_str()) + "\n";
                }
            }
            return _str + "\n";
        }
        else return "No avaliable OpenCL platforms and devices\n";
    }

    public: Q_SIGNAL void SIGNAL_WriteUIString(const QString message);
    ///< try to chatch it with UI

    public: Q_SIGNAL void SIGNAL_WriteToLog(const QString message);
    ///< Signal for writing data to log file

    /// Emits simple strings, that shows platforms info
    public: Q_SLOT void SLOT_ShowPlatformsInfo()
    {
        Q_EMIT SIGNAL_WriteToLog(getPlatformsInfo());
    }
    /// Emits simple strings, that shows platforms info
    public: Q_SLOT void SLOT_ShowDevicesInfo()
    {
        Q_EMIT SIGNAL_WriteToLog(getDevicesInfo());
    }

    public: ~OpenCLManager()
    {
        if(commandQueues != nullptr)
            delete [] commandQueues;

        if(devices != nullptr)
            delete [] devices;
    }
    static OpenCLManager& getInstance()
    {
        static OpenCLManager _singleInstanceOfOpenCLManager;
        return _singleInstanceOfOpenCLManager;
    }
};

/*void runCl()
{
    cl_int err = CL_SUCCESS;
    //try {
        std::vector<cl::Platform> platforms;
        cl::Platform::get(&platforms);
        // platforms: Intel, AMD, NVidia, etc.
        if (platforms.size() == 0)
        {
            printf("Platform size 0\n");
            return;
        }

        cl_context_properties properties[] = {
            CL_CONTEXT_PLATFORM,
            (cl_context_properties)(platforms[0])(),
            0};
        cl::Context context(CL_DEVICE_TYPE_CPU, properties);
        // context: CPU, GPU, AXELLERATOR, etc.

        std::vector<cl::Device> devices = context.getInfo<CL_CONTEXT_DEVICES>();
        // devices: specific devices of the platfom and context
        // they do all dirty work :)

        cl::Program::Sources source(1,std::make_pair(helloStr,strlen(helloStr)));
        cl::Program program_ = cl::Program(context, source);
        program_.build(devices);
        cl::Kernel kernel(program_, "hello", &err);
        cl::Event event;
        // kernel: it is the code that should be done on devices


        cl::CommandQueue queue(context, devices[0], 0, &err);
        // queue - queue of commands for specific device context

        queue.enqueueNDRangeKernel(
                    kernel,
                    cl::NullRange,
                    cl::NDRange(4,4),
                    cl::NullRange,
                    NULL,
                    &event);
        event.wait();
    }
    catch (cl::Error err) {
        std::cerr
                << "ERROR: "
                << err.what()
                << "("
                << err.err()
                << ")"
                << std::endl;
    }
}*/

#endif // OPENCLMANAGER_H
